
package server;

import SBLib.File;
import SBLib.JsonFile;
import SBLib.User;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Hashtable;

import java.util.logging.Level;
import java.util.logging.Logger;


import org.json.simple.JSONObject;




/***
 * Class to handle all of the requests that come from the client.
 * 
 * - Replication occurs for new user, new file, edit file, rename file, delete file
 * 
 * @author BrandonNeal
 */
public class RequestHandler
{
    private String[] chunks;//represenation of client requests. They come in patter of --> something&&&somethingElse&&&anotherThing. So it is an array of the meaningfull datat
    private String returnMessage="";
    
    //private Hashtable required = new Hashtable();
    
    final private ArrayList<File> Files;
    final private ArrayList<User> Users;
    //public Replicator replicator;
    //User currUser;
    Response response;
       
    /***
     * 
     * @param request - string input from client
     * @param users - ArrayList of shared users of the system
     * @param files  - ArrayList of shared files of the the system
     */
    public RequestHandler(ArrayList<User> users, ArrayList<File> files)
    {
        
        this.Users = users;
        this.Files = files;
        response = new Response();
        //this.replicator = replicator;
        
    }
    
    
    /***
     * - Splits the clients request on &&& to get the meaningful data out of it. (ie. VIEW&&&filename --> Array(0=>"VIEW",1=>"filename");
     * - gets the request type from the chunks array.
     * - To avoid using a big if...else if...else if... etc block, this method uses Reflection to know which method to call.
     *      - Each request type corresponds to a method type of the exact same name but with lower case. (ie. VIEW -> view, DELETE->delete etc..)
     *      - This also takes care of error handling, because if the client sends an invalid request, then the Reflection will fail and will throw NoSuchMethodException
     * 
     * NOTE: There is no error check (yet) to ensure that there is enough data sent by client for each request.
     * @return - String to out put back to client
     */
    public synchronized String HandleRequest(String request) 
    {
        this.chunks=request.split(Server.SPLIT); //split the request string to get the meaningfun data
        String type = this.getChunk(0); //get request type
        
        try {
            try {
                
                this.getClass().getMethod(type.toLowerCase()).invoke(this); //Use reflection to call the function that the request is for
           
            } 
            catch (IllegalAccessException ex) {
                Logger.getLogger(RequestHandler.class.getName()).log(Level.SEVERE, null, ex);
            } catch (IllegalArgumentException ex) {
                Logger.getLogger(RequestHandler.class.getName()).log(Level.SEVERE, null, ex);
            } catch (InvocationTargetException ex) {
                Logger.getLogger(RequestHandler.class.getName()).log(Level.SEVERE, null, ex);
            }
                    
        } 
        //Comes here if the method was not found
        catch (NoSuchMethodException ex) {
            
            this.InvalidRequest(); //Then the request was invalid            
            Logger.getLogger(RequestHandler.class.getName()).log(Level.SEVERE, null, ex);
            
        } catch (SecurityException ex) {
            Logger.getLogger(RequestHandler.class.getName()).log(Level.SEVERE, null, ex);
        }
        
        return this.returnMessage;
    }
    /**
     * Sets return message to invalid request.
     */
    public void InvalidRequest()
    {
        this.setReturnMessage(response.ErrorResponse("Invalid request"));
    }
    /**
     * - Registers a new user to the system. username must be unique in order to register. If it is, adds the new User to the Users list (which is shared among
     *      all threads in the Server.
     * - ADDED REPLICATION. MORE WORK IN FUTURE TODO
     */
    public void register()
    {
        synchronized(this.Users){
            
            String username = this.getChunk(1);
            String password = this.getChunk(2);
            User user = new User(username, password);

            if(this.usernameExists(user)) //If user exists        
            {
                this.setReturnMessage(response.FailedResponse(Response.FailCode.USER_INVALID_REGISTER));
            }
            else
            {
                //replicator.new_user(username, password); 
                user.SaveUser(); //saves User to disk
                this.Users.add(user);
                this.setReturnMessage(response.SuccessResponse());
            }
                            
        }
    }
    /**
     * Attempts to log the user in. 
     */
    public void login()
    {
        synchronized(this.Users){
            String username = this.getChunk(1);
            String password = this.getChunk(2);
            User user = new User(username, password);
            
            if(this.validLogin(user))
            {
                
                //this.currUser.setUserName(username);
                //this.currUser.setPassword(password);
                                
                this.setReturnMessage(response.SuccessResponse());
            }
            else
            {
                this.setReturnMessage(response.FailedResponse(Response.FailCode.USER_INVALID_LOGIN));
            }
        }
        
    }
    
    /**
     * Returns JSON encoded string of all the File's filename:filecontent
     */
    public void get_listing()
    {
        synchronized(this.Files){
           JSONObject json = JsonFile.FileListToJson(this.Files);   
           this.setReturnMessage(response.Response(json.toString()));
        }
    }
    /**
     * - Creates a file. 
     * - Filename MUST be unique. If it is, adds the new File to the File list that is shared among all threads in the Server
     * - ADDED REPLICATION. MORE WORK IN FUTURE TODO
     */
    public void create()
    {
        synchronized(this.Files){
            String filename = this.getChunk(1);
            String content = this.getChunk(2);
            String username = this.getChunk(3);
            
            File file = new File(filename, content, username);
            
            if(this.Files.indexOf(file) >= 0) //Check if file exists. If it does, then send error message.
            {
                this.setReturnMessage(response.FailedResponse(Response.FailCode.FILE_EXISTS));
            }
            else{
                
                //replicator.new_file(filename, content);                        
                file.SaveFile();
                this.Files.add(file);
                this.setReturnMessage(response.SuccessResponse());
            }
        
        }
    }
    
    /**
     * - Get the file content, and also add lock to the File.
     * 
     */
    public void view()
    {
        synchronized(this.Files){
            
            File file = new File();
            file.setFileName(this.getChunk(1));
            String username = this.getChunk(2);
            int fileId;
            
            if((fileId = this.fileExists(file)) >= 0 && this.lockFile(this.Files.get(fileId), username))//get fileId if it exists, then add user to the File's lock
            {
                //replicator.lock_file(this.Files.get(fileId).getFileName(), username);
                File toView = this.Files.get(fileId);
                System.out.println(toView.LockLogger()); //debugging purposes
                this.setReturnMessage(response.SuccessResponse(toView.getFileContent()));    
            }
           
        }
    }
   
    /**
     * - Deletes file from the Files list.
     * - The current user must be the creator of the file, and the File cannot have any locks on it (no one else has it open)
     * 
     * - ADDED REPLICATION. MORE WORK IN FUTURE TODO
     */
    public void delete()
    {
        synchronized(this.Files){
            File file = new File();
            file.setFileName(this.getChunk(1));
            //file.setUserName(this.getChunk(2));
            String username = this.getChunk(2);
            int fileId;
            if((fileId = this.fileExists(file)) >= 0 && this.hasPermissions(file) && this.fileUnlocked(this.Files.get(fileId), username))        
            {
               //replicator.delete_file(file.getFileName());
               file.DeleteFile();
               this.Files.remove(file);
               this.setReturnMessage(response.SuccessResponse());
            }
            
        }
    }
     /**
     * - Rename file 
     * - The current user must be the creator of the file, and the File cannot have any locks on it (no one else has it open)
     * 
     * - ADDED REPLICATION. MORE WORK IN FUTURE TODO
     */
    public void rename()
    {
        synchronized(this.Files){
            File file = new File();
            file.setFileName(this.getChunk(1));
            String newName = this.getChunk(2);
            //file.setUserName(this.getChunk(3));
            String username = this.getChunk(3);

            int fileId;
            if((fileId = this.fileExists(file)) >= 0 && this.hasPermissions(file) && this.fileUnlocked(this.Files.get(fileId),username) )
            {
               //replicator.rename_file(file.getFileName(), newName);
               this.Files.get(this.Files.indexOf(file)).RenameFile(newName);
               this.setReturnMessage(response.SuccessResponse());
            }
            
        }
    }
    /**
     * - Edit the File's content
     * - There cannot be a lock on the file.
     * 
     * - ADDED REPLICATION. MORE WORK IN FUTURE TODO
     */
    public void edit()
    {
        synchronized(this.Files){
            File file = new File();
            file.setFileName(this.getChunk(1));
            file.setFileContent(this.getChunk(2));
            //file.setUserName(this.getChunk(3));
            String username = this.getChunk(3);
            int fileId;
            
            if((fileId = this.fileExists(file)) >= 0 && this.fileUnlocked(this.Files.get(fileId),username))//this.lockFile(this.Files.get(fileId), file.getUserName()) )
            {
                //replicator.edit_file(file.getFileName(), file.getFileContent());
                this.Files.get(fileId).EditFile(file.getFileContent());
                this.setReturnMessage(response.SuccessResponse());
            }
           
        }
    }
    /**
     * - Remove current user from the File's list of users that currently have a lock on the File.
     */
    public void close()
    {
        synchronized(this.Files){
            
            File file = new File();
            file.setFileName(this.getChunk(1));
            String username = this.getChunk(2);
            int fileId;
            if((fileId = this.fileExists(file)) >= 0 )
            {
                //replicator.unlock_file(this.getChunk(1), username);
                this.Files.get(fileId).Unlock(username);
                this.setReturnMessage(response.SuccessResponse());
            }
            else
                this.setReturnMessage(response.SuccessResponse());
        }
    }
    
    /// Helper Functions
    /**
     * See if a given File object exists in the list of Files
     * @param file File object to see if it exists
     * @return index of the File object in the File list
     */
    private int fileExists(File file)
    {
        int fileId = this.Files.indexOf(file);
        if(fileId > 0)
        {
            this.setReturnMessage(response.FailedResponse(Response.FailCode.FILE_NOT_FOUND));
        }
        return fileId;
    }
    /**
     * See if the current user has the required permissions (was the creator) of the File object passed in.
     * @param file File object of file to verify permissions against
     * @return boolean whether or not current user has permission to edit/rename
     */
    private boolean hasPermissions(File file)
     {
         return true;
         
        /*Boolean b = this.Files.get(this.Files.indexOf(file)).getUserName().equals(file.getUserName());
         if(!b)
         {
             this.setReturnMessage(response.FailedResponse(Response.FailCode.FILE_PERMISSIONS));
         }
         return b;*/
     }
    /**
     * Add the given username to the given File's list of users that have a lock on the file.
     * @param file File object to add user to it's list of users who have locks on file.
     * @param username Name of user to add to File objects list of users that have locks on the file.
     * @return success of attaining lock. Will always be true, but too scared to change close to deadline.
     */
    private boolean lockFile(File file, String username)
    {
        Boolean b = file.Lock(username);
        if(!b)//don't know how it could get here
        {
            this.setReturnMessage(response.FailedResponse(Response.FailCode.FILE_LOCKED));
        }
        
        return b;
    }
    /**
     * Checks if the file is currently Unlocked (it has no locks on it).
     * @param file File object to see if at least 1 user has a lock on the File.
     * @return 
     */
    private boolean fileUnlocked(File file, String username)
    {
        Boolean b = file.getLocked(username);
        if(b)//if already locked
        {
            this.setReturnMessage(response.FailedResponse(Response.FailCode.FILE_LOCKED));
        }
        return !b; //if b=true, then the file is Locked. So return !b (false), because it is false that the file is Unlocked
    }
    /**
     * Checks if a given username already exists in the system
     * @param user User object
     * @return true if username already exists, false if it is unique
     */
    private Boolean usernameExists(User user)
    {
        for(User u:this.Users)
        {
            if(u.getUserName().equals(user.getUserName()))
            {
                return true;
            } 
        }
        return false;
    }
    /**
     * Checks if the User provided a valid username and password to login 
     * @param user
     * @return 
     */
    public Boolean validLogin(User user)
    {
        
        for(User u:this.Users)
        {
            if(u.getUserName().equals(user.getUserName()) && u.getPassword().equals(user.getPassword()))
            {
                return true;
            } 
        }
        return false;
    }
    /**
     * Only called when the thread gets reset, which will occur when the client closes there application. 
     * Go through all of the files and remove any locks with this user name;
     */
   /* public void RemoveUserLocks()
    {
        synchronized(this.Files)
        {
            for(File f: this.Files)
            {
                f.Unlock(this.currUser.getUserName());
            }
        }
    }*/
     
    private void setReturnMessage(String message)
    {
        this.returnMessage=message;
    }
    private String getChunk(int i)
    {
        return this.chunks[i];
    }
    

}
